Vue 3 中 v 您所在的位置:网站首页 v if else Vue 3 中 v

Vue 3 中 v

2024-03-04 17:42| 来源: 网络整理| 查看: 265

前言

又回到了经典的一句话:“知其然,而后使其然”。相信大家对 Vue 提供 v-if 和 v-show 指令的使用以及对应场景应该都滚瓜烂熟了。但是,我想仍然会有很多同学对于 v-if 和 v-show 指令实现的原理存在知识空白。

所以,今天就让我们来一起了解一番 v-if 和 v-show 指令实现的原理~

v-if

在之前 【Vue3 源码解读】从编译过程,理解静态节点提升 一文中,我给大家介绍了 Vue 3 的编译过程,即一个模版会经历 baseParse、transform、generate 这三个过程,最后由 generate 生成可以执行的代码(render 函数)。

这里,我们就不从编译过程开始讲解 v-if 指令的 render 函数生成过程了,有兴趣了解这个过程的同学,可以看我之前的文章从编译过程,理解静态节点提升

我们可以直接在 Vue3 Template Explore 输入一个使用 v-if 指令的栗子:

然后,由它编译生成的 render 函数会是这样:

render(_ctx, _cache, $props, $setup, $data, $options) { return (_ctx.visible) ? (_openBlock(), _createBlock("div", { key: 0 })) : _createCommentVNode("v-if", true) }

可以看到,一个简单的使用 v-if 指令的模版编译生成的 render 函数最终会返回一个三目运算表达式。首先,让我们先来认识一下其中几个变量和函数的意义:

_ctx 当前组件实例的上下文,即 this_openBlock() 和 _createBlock() 用于构造 Block Tree 和 Block VNode,它们主要用于靶向更新过程_createCommentVNode() 创建注释节点的函数,通常用于占位

显然,如果当 visible 为 false 的时候,会在当前模版中创建一个注释节点(也可称为占位节点),反之则创建一个真实节点(即它自己)。例如当 visible 为 false 时渲染到页面上会是这样:

在 Vue 中很多地方都运用了注释节点来作为占位节点,其目的是在不展示该元素的时候,标识其在页面中的位置,以便在 patch 的时候将该元素放回该位置。

那么,这个时候我想大家就会抛出一个疑问:当 visible 动态切换 true 或 false 的这个过程(派发更新)究竟发生了什么?

派发更新时 patch,更新节点如果不了解 Vue 3 派发更新和依赖收集过程的同学,可以看我之前的文章4k+ 字分析 Vue 3.0 响应式原理(依赖收集和派发更新)

在 Vue 3 中总共有四种指令:v-on、v-model、v-show 和 v-if。但是,实际上在源码中,只针对前面三者进行了特殊处理,这可以在 packages/runtime-dom/src/directives 目录下的文件看出:

// packages/runtime-dom/src/directives |-- driectives |-- vModel.ts ## v-model 指令相关 |-- vOn.ts ## v-on 指令相关 |-- vShow.ts ## v-show 指令相关

而针对 v-if 指令是直接走派发更新过程时 patch 的逻辑。由于 v-if 指令订阅了 visible 变量,所以当 visible 变化的时候,则会触发派发更新,即 Proxy 对象的 set 逻辑,最后会命中 componentEffect 的逻辑。

当然,我们也可以称这个过程为组件的更新过程

这里,我们来看一下 componentEffect 的定义(伪代码):

function componentEffect() { if (!instance.isMounted) { .... } else { ... const nextTree = renderComponentRoot(instance) const prevTree = instance.subTree instance.subTree = nextTree patch( prevTree, nextTree, hostParentNode(prevTree.el!)!, getNextHostNode(prevTree), instance, parentSuspense, isSVG ) ... } } }

可以看到,当组件还没挂载时,即第一次触发派发更新会命中 !instance.isMounted 的逻辑。而对于我们这个栗子,则会命中 else 的逻辑,即组件更新,主要会做三件事:

获取当前组件对应的组件树 nextTree 和之前的组件树 prevTree更新当前组件实例 instance 的组件树 subTree 为 nextTreepatch 新旧组件树 prevTree 和 nextTree,如果存在 dynamicChildren,即 Block Tree,则会命中靶向更新的逻辑,显然我们此时满足条件注:组件树则指的是该组件对应的 VNode Tree。小结

总体来看,v-if 指令的实现较为简单,基于数据驱动的理念,当 v-if 指令对应的 value 为 false 的时候会预先创建一个注释节点在该位置,然后在 value 发生变化时,命中派发更新的逻辑,对新旧组件树进行 patch,从而完成使用 v-if 指令元素的动态显示隐藏。

下面,我们来看一下 v-show 指令的实现~v-show

同样地,对于 v-show 指令,我们在 Vue 3 在线模版编译平台输入这样一个栗子:

那么,由它编译生成的 render 函数:

render(_ctx, _cache, $props, $setup, $data, $options) { return _withDirectives((_openBlock(), _createBlock("div", null, null, 512 /* NEED_PATCH */)), [ [_vShow, _ctx.visible] ]) }

此时,这个栗子在 visible 为 false 时,渲染到页面上的 HTML:

从上面的 render 函数可以看出,不同于 v-if 的三目运算符表达式,v-show 的 render 函数返回的是 _withDirectives() 函数的执行。

前面,我们已经简单介绍了 _openBlock() 和 _createBlock() 函数。那么,除开这两者,接下来我们逐点分析一下这个 render 函数,首当其冲的是 vShow ~

vShow 在生命周期中改变 display 属性

_vShow 在源码中则对应着 vShow,它被定义在 packages/runtime-dom/src/directives/vShow。它的职责是对 v-show 指令进行特殊处理,主要表现在 beforeMount、mounted、updated、beforeUnMount 这四个生命周期中:

// packages/runtime-dom/src/directives/vShow export const vShow: ObjectDirective = { beforeMount(el, { value }, { transition }) { el._vod = el.style.display === 'none' ? '' : el.style.display if (transition && value) { // 处理 tansition 逻辑 ... } else { setDisplay(el, value) } }, mounted(el, { value }, { transition }) { if (transition && value) { // 处理 tansition 逻辑 ... } }, updated(el, { value, oldValue }, { transition }) { if (!value === !oldValue) return if (transition) { // 处理 tansition 逻辑 ... } else { setDisplay(el, value) } }, beforeUnmount(el, { value }) { setDisplay(el, value) } }

对于 v-show 指令会处理两个逻辑:普通 v-show 或 transition 时的 v-show 情况。通常情况下我们只是使用 v-show 指令,命中的就是前者。

这里我们只对普通 v-show 情况展开分析。

普通 v-show 情况,都是调用的 setDisplay() 函数,以及会传入两个变量:

el 当前使用 v-show 指令的真实元素v-show 指令对应的 value 的值

接着,我们来看一下 setDisplay() 函数的定义:

function setDisplay(el: VShowElement, value: unknown): void { el.style.display = value ? el._vod : 'none' }

setDisplay() 函数正如它本身命名的语意一样,是通过改变该元素的 CSS 属性 display 的值来动态的控制 v-show 绑定的元素的显示或隐藏。

并且,我想大家可能注意到了,当 value 为 true 的时候,display 是等于的 el.vod,而 el.vod 则等于这个真实元素的 CSS display 属性(默认情况下为空)。所以,当 v-show 对应的 value 为 true 的时候,元素显示与否是取决于它本身的 CSS display 属性。

其实,到这里 v-show 指令的本质在源码中的体现已经出来了。但是,仍然会留有一些疑问,例如 withDirectives 做了什么?vShow 在生命周期中对 v-show 指令的处理又是如何运用的?withDirectives 在 VNode 上增加 dir 属性

withDirectives() 顾名思义和指令相关,即在 Vue 3 中和指令相关的元素,最后生成的 render 函数都会调用 withDirectives() 处理指令相关的逻辑,将 vShow 的逻辑作为 dir 属性添加到 VNode 上。

withDirectives() 函数的定义:

// packages/runtime-core/directives export function withDirectives( vnode: T, directives: DirectiveArguments ): T { const internalInstance = currentRenderingInstance if (internalInstance === null) { __DEV__ && warn(`withDirectives can only be used inside render functions.`) return vnode } const instance = internalInstance.proxy const bindings: DirectiveBinding[] = vnode.dirs || (vnode.dirs = []) for (let i = 0; i < directives.length; i++) { let [dir, value, arg, modifiers = EMPTY_OBJ] = directives[i] if (isFunction(dir)) { ... } bindings.push({ dir, instance, value, oldValue: void 0, arg, modifiers }) } return vnode }

首先,withDirectives() 会获取当前渲染实例处理边缘条件,即如果在 render 函数外面使用 withDirectives() 则会抛出异常:

"withDirectives can only be used inside render functions."

然后,在 vnode 上绑定 dirs 属性,并且遍历传入的 directives 数组,而对于我们这个栗子 directives 就是:

[ [_vShow, _ctx.visible] ]

显然此时只会迭代一次(数组长度为 1)。并且从 render 传入的 参数可以知道,从 directives 上解构出的 dir 指的是 _vShow,即我们上面介绍的 vShow。由于 vShow 是一个对象,所以会重新构造(bindings.push())一个 dir 给 VNode.dir。

VNode.dir 的作用体现在 vShow 在生命周期改变元素的 CSS display 属性,而这些生命周期会作为派发更新的结束回调被调用。

接下来,我们一起来看看其中的调用细节~派发更新时 patch,注册 postRenderEffect 事件

相信大家应该都知道 Vue 3 提出了 patchFlag 的概念,其用来针对不同的场景来执行对应的 patch 逻辑。那么,对于上面这个栗子,我们会命中 patchElement 的逻辑。

而对于 v-show 之类的指令来说,由于 Vnode.dir 上绑定了处理元素 CSS display 属性的相关逻辑( vShow 定义好的生命周期处理)。所以,此时 patchElement 中会为注册一个 postRenderEffect 事件。

const patchElement = ( n1: VNode, n2: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) => { ... // 此时 dirs 是存在的 if ((vnodeHook = newProps.onVnodeUpdated) || dirs) { // 注册 postRenderEffect 事件 queuePostRenderEffect(() => { vnodeHook && invokeVNodeHook(vnodeHook, parentComponent, n2, n1) dirs && invokeDirectiveHook(n2, n1, parentComponent, 'updated') }, parentSuspense) } ... }

这里我们简单分析一下 queuePostRenderEffect 和 invokeDirectiveHook:

queuePostRenderEffect,postRenderEffect 事件注册是通过 queuePostRenderEffect 完成的,因为 effect 都是维护在一个队列中(为了保持 effect 的有序),这里是 pendingPostFlushCbs,所以对于 postRenderEffect 也是一样的会被进队invokeDirectiveHook,由于 vShow 封装了对元素 CSS display 属性的处理,所以 invokeDirective 的本职是调用指令相关的生命周期处理。并且,需要注意的是此时是更新逻辑,所以只会调用 vShow 中定义好的 update 生命周期flushJobs 的结束(finally)调用 postRenderEffect

到这里,我们已经围绕 v-Show 介绍完了 vShow、withDirectives、postRenderEffect 等概念。但是,万事具备只欠东风,还缺少一个调用 postRenderEffect 事件的时机,即处理 pendingPostFlushCbs 队列的时机.

在 Vue 3 中 effect 相当于 Vue 2.x 的 watch。虽然变了个命名,但是仍然保持着一样的调用方式,都是调用的 run() 函数,然后由 flushJobs() 执行 effect 队列。而调用 postRenderEffect 事件的时机则是在执行队列的结束。

flushJobs 函数的定义:

// packages/runtime-core/scheduler.ts function flushJobs(seen?: CountMap) { isFlushPending = false isFlushing = true if (__DEV__) { seen = seen || new Map() } flushPreFlushCbs(seen) // 对 effect 进行排序 queue.sort((a, b) => getId(a!) - getId(b!)) try { for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { // 执行渲染 effect const job = queue[flushIndex] if (job) { ... } } } finally { ... // postRenderEffect 事件的执行时机 flushPostFlushCbs(seen) ... } }

在 flushJobs() 函数中会执行三种 effect 队列,分别是 preRenderEffect、renderEffect、postRenderEffect,它们各自对应 flushPreFlushCbs()、queue、flushPostFlushCbs。

那么,显然 postRenderEffect 事件的调用时机是在 flushPostFlushCbs()。而 flushPostFlushCbs() 内部则会遍历 pendingPostFlushCbs 队列,即执行之前在 patchElement 时注册的 postRenderEffect 事件,本质上就是执行:

updated(el, { value, oldValue }, { transition }) { if (!value === !oldValue) return if (transition) { ... } else { // 改变元素的 CSS display 属性 setDisplay(el, value) } },小结

相比较 v-if 简单干脆地通过 patch 直接更新元素,v-show 的处理就略显复杂。这里我们重新梳理一下整个过程:

首先,由 widthDirectives 来生成最终的 VNode。它会给 VNode 上绑定 dir 属性,即 vShow 定义的在生命周期中对元素 CSS display 属性的处理其次,在 patchElement 的阶段,会注册 postRenderEffect 事件,用于调用 vShow 定义的 update 生命周期处理 CSS display 属性的逻辑最后,在派发更新的结束,调用 postRenderEffect 事件,即执行 vShow 定义的 update 生命周期,更改元素的 CSS display 属性

结语

v-if 和 v-show 原理,你可以用一两句话概括,也可以用一大堆话概括。如果牵扯到面试场景下,我更欣赏后者,因为这说明你研究的够深以及理解能力够强。并且,当你了解一个指令的处理过程后,对于其他指令 v-on、v-model 的处理,相信也可以很容易的得出结论。最后,如果文中存在表达不当或错误的地方,欢迎各位同学提 Issue~

我是五柳,喜欢创新、捣鼓源码,专注于源码(Vue 3、Vite)、前端工程化、跨端等技术学习和分享。此外,我的所有文章都会收录在 https://github.com/WJCHumble/Blog,欢迎 Watch Or Star!


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有